package vip.wangwenhao.service.impl;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import vip.wangwenhao.annotation.DefinitionQuery;
import vip.wangwenhao.document.BaseDocument;
import vip.wangwenhao.enums.QueryTypeEnum;
import vip.wangwenhao.query.BaseQuery;
import vip.wangwenhao.mapping.KeyMapping;
import vip.wangwenhao.model.FieldDefinition;
import vip.wangwenhao.model.Key;
import vip.wangwenhao.page.PageParam;
import vip.wangwenhao.page.PageResult;
import vip.wangwenhao.page.SortParam;
import vip.wangwenhao.service.EsService;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import static vip.wangwenhao.constants.CommonConstants.*;

/**
 * @author wwh
 */

@Slf4j
public class EsBaseServiceImpl<T extends BaseDocument> implements EsService<T> {

    @Resource
    private ElasticsearchRestTemplate elasticsearchTemplate;
    @Autowired
    private RestHighLevelClient restHighLevelClient;

    private Map<Key, FieldDefinition> keyMappings;


    @PostConstruct
    private void init() {
        Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        createIndex(entityClass);

        log.info("Mapping queries");
        keyMappings = KeyMapping.mapping(entityClass);
        keyMappings.forEach((k, v) -> log.info("Mapped query {}", v));
    }


    @Override
    public Boolean createIndex(Class<T> clazz) {
        try {
            if (!elasticsearchTemplate.indexExists(clazz)) {
                log.info("创建索引，clazz:{}", clazz);
                boolean result = elasticsearchTemplate.createIndex(clazz);
                if (!result) {
                    log.warn("创建索引失败，clazz:{}", clazz);
                }
                log.info("创建索引成功，clazz:{}", clazz);
            }

            log.info("创建Mapping映射，clazz:{}", clazz);
            boolean result = elasticsearchTemplate.putMapping(clazz);
            if (!result) {
                log.warn("创建Mapping映射失败，clazz:{}", clazz);
            }
            log.info("创建Mapping映射成功，clazz:{}", clazz);

            return true;
        } catch (Exception e) {
            log.error("创建索引失败,class:{}", clazz, e);
            return false;
        }
    }

    @Override
    public Boolean deleteIndex(Class<T> clazz) {
        return elasticsearchTemplate.deleteIndex(clazz);
    }

    @Override
    public Boolean index(T t) {
        IndexQuery query = new IndexQuery();
        query.setId(t.getId());
        query.setObject(t);
        Document document = t.getClass().getAnnotation(Document.class);
        String indexName = document.indexName();
        String id = elasticsearchTemplate.index(query, IndexCoordinates.of(indexName));
        t.setId(id);
        return true;
    }

    @Override
    public Boolean batchIndex(List<T> ts) {
        if (null != ts && ts.size() > 0) {
            List<IndexQuery> queries = new ArrayList<>();
            for (T t : ts) {
                IndexQuery query = new IndexQuery();
                query.setId(t.getId());
                query.setObject(t);
            }
            Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            Document document = entityClass.getAnnotation(Document.class);
            String indexName = document.indexName();
            List<String> ids = elasticsearchTemplate.bulkIndex(queries, IndexCoordinates.of(indexName));
            for (int i = 0; i < ids.size(); i++) {
                T t = ts.get(i);
                String id = ids.get(i);
                t.setId(id);
            }
            return true;
        }
        return false;
    }

    @Override
    public T get(String id) {
        Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        return elasticsearchTemplate.get(id, entityClass);
    }

    @Override
    public List<T> queryAll() {
        Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        Document document = entityClass.getAnnotation(Document.class);
        String indexName = document.indexName();
        SearchRequest request = new SearchRequest(indexName);
        request.source(new SearchSourceBuilder());
        try {
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            SearchHit[] hits = response.getHits().getHits();
            List<T> res = new ArrayList<>(hits.length);
            for (SearchHit hit : hits) {
                res.add(JSON.parseObject(hit.getSourceAsString(), entityClass));
            }
            return res;
        } catch (Exception e) {
            log.error("queryAll error", e);
            return null;
        }
    }

    @Override
    public List<T> queryForList(T t) {

        try {
            Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            Document document = entityClass.getAnnotation(Document.class);
            String indexName = document.indexName();
            SearchRequest request = new SearchRequest(indexName);
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
            Field[] declaredFields = t.getClass().getDeclaredFields();
            if (!CollectionUtils.isEmpty(declaredFields)) {
                for (Field field : declaredFields) {
                    field.setAccessible(true);
                    Object obj = field.get(t);

                    if (null != obj) {
                        String value = String.valueOf(obj);
                        org.springframework.data.elasticsearch.annotations.Field annotation = field.getAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);

                        if (null != annotation) {
                            String key = annotation.value();
                            if (StringUtils.isEmpty(key)) {
                                key = field.getName();
                            }
                            boolQueryBuilder.must(QueryBuilders.wildcardQuery(key, value));

                        }
                    }

                }
            }
            sourceBuilder.query(boolQueryBuilder);
            request.source(sourceBuilder);
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            SearchHit[] hits = response.getHits().getHits();
            List<T> res = new ArrayList<>(hits.length);
            for (SearchHit hit : hits) {
                res.add(JSON.parseObject(hit.getSourceAsString(), entityClass));
            }
            return res;
        } catch (Exception e) {
            log.error("queryForList error", e);
            return null;
        }
    }

    @Override
    public PageResult<T> queryForPage(BaseQuery<T> query) {
        try {

            T t = query.getQuery();
            PageParam pageParam = query.getPageParam();


            Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            Document document = entityClass.getAnnotation(Document.class);
            String indexName = document.indexName();
            SearchRequest request = new SearchRequest(indexName);
            SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
            BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
            Field[] declaredFields = entityClass.getDeclaredFields();
            if (!CollectionUtils.isEmpty(declaredFields)) {
                for (Field field : declaredFields) {
                    field.setAccessible(true);

                    Object obj = field.get(t);


                    if (null != obj) {
                        String value = String.valueOf(obj);
                        org.springframework.data.elasticsearch.annotations.Field annotation = field.getAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
                        if (null != annotation) {
                            String fieldName = annotation.value();
                            if (StringUtils.isEmpty(fieldName)) {
                                fieldName = field.getName();
                            }
                            boolQueryBuilder.must(QueryBuilders.wildcardQuery(fieldName, value));

                        }
                    }
                }
            }
            List<SortParam> sortParams = pageParam.getSortParams();
            if (!org.springframework.util.CollectionUtils.isEmpty(sortParams)) {
                for (SortParam sortParam : sortParams) {
                    sourceBuilder.sort(SortBuilders.fieldSort(sortParam.getSortField()).order(sortParam.getSortOrder()));
                }
            }
            sourceBuilder.query(boolQueryBuilder);
            sourceBuilder.from((pageParam.getPageNo() - 1) * pageParam.getPageSize());
            sourceBuilder.size(pageParam.getPageSize());
            request.source(sourceBuilder);
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            SearchHit[] hits = response.getHits().getHits();
            List<T> res = new ArrayList<>(hits.length);
            for (SearchHit hit : hits) {
                res.add(JSON.parseObject(hit.getSourceAsString(), entityClass));
            }
            long count = response.getHits().getTotalHits().value;
            return PageResult.success(res, count);
        } catch (Exception e) {
            log.error("queryForPage error", e);
            return PageResult.fail();
        }
    }

    @Override
    public PageResult<T> query(BaseQuery<T> query) {
        try {
            T t = query.getQuery();
            PageParam pageParam = query.getPageParam();

            Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            SearchRequest request = buildSearchRequest(t, pageParam);
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            SearchHit[] hits = response.getHits().getHits();
            List<T> res = new ArrayList<>(hits.length);
            for (SearchHit hit : hits) {
                res.add(JSON.parseObject(hit.getSourceAsString(), entityClass));
            }
            long count = response.getHits().getTotalHits().value;
            return PageResult.success(res, count);
        } catch (Exception e) {
            log.error("queryForPage error", e);
            return PageResult.fail();
        }
    }

    @Override
    public Boolean delete(String id) {
        try {
            Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
            Document document = entityClass.getAnnotation(Document.class);
            String indexName = document.indexName();
            DeleteRequest deleteRequest = new DeleteRequest(indexName);
            deleteRequest.id(id);
            DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
            return deleteResponse.status().equals(RestStatus.OK);
        } catch (IOException e) {
            return false;
        }
    }

    public SearchRequest buildSearchRequest(T t, PageParam pageParam) {
        Class<T> entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        Document document = entityClass.getAnnotation(Document.class);
        String indexName = document.indexName();
        SearchRequest request = new SearchRequest(indexName);
        SearchSourceBuilder searchSourceBuilder = buildSearchQuery(t, pageParam);
        request.source(searchSourceBuilder);
        return request;
    }

    public SearchSourceBuilder buildSearchQuery(T t, PageParam pageParam) {

        try {
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            BoolQueryBuilder boolFilterBuilder = QueryBuilders.boolQuery();
            AtomicBoolean matchSearch = new AtomicBoolean(false);
            Map<String, BoolQueryBuilder> nestedMustMap = new HashMap<>();
            Map<String, BoolQueryBuilder> nestedMustNotMap = new HashMap<>();
            List<String> fullTextFieldList = new ArrayList<>();

            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
                    .from((pageParam.getPageNo() - 1) * pageParam.getPageSize())
                    .size(pageParam.getPageSize());

            Field[] declaredFields = t.getClass().getDeclaredFields();
            Map<String, String> params = new HashMap<>();
            if (!CollectionUtils.isEmpty(declaredFields)) {
                for (Field field : declaredFields) {
                    field.setAccessible(true);
                    Object obj = field.get(t);

                    if (null != obj) {
                        String key = null;
                        String value = String.valueOf(obj);
                        DefinitionQuery definitionQuery = field.getAnnotation(DefinitionQuery.class);
                        if (null != definitionQuery) {
                            key = definitionQuery.key();
                            if (StringUtils.isBlank(key)) {
                                org.springframework.data.elasticsearch.annotations.Field fieldAnnotation = field.getAnnotation(org.springframework.data.elasticsearch.annotations.Field.class);
                                if (null != fieldAnnotation) {
                                    key = fieldAnnotation.value();
                                }
                            }

                        }
                        if (StringUtils.isEmpty(key)) {
                            key = field.getName();
                        }
                        params.put(key, value);
                    }
                }
            }
            Map<String, String> ext = pageParam.getExt();
            if (!org.springframework.util.CollectionUtils.isEmpty(ext)) {
                params.putAll(ext);
            }
            String q = params.get(Q);
            String missingFields = params.get(MISSING);
            String existsFields = params.get(EXISTS);
            keyMappings
                    .entrySet()
                    .stream()
                    .filter(m -> m.getValue().getQueryType() == QueryTypeEnum.FULLTEXT
                            || m.getValue().getQueryType() != QueryTypeEnum.IGNORE
                            && params.get(m.getKey().toString()) != null)
                    .forEach(m -> {
                        String k = m.getKey().toString();
                        FieldDefinition v = m.getValue();
                        String queryValue = params.get(k);
                        QueryTypeEnum queryType = v.getQueryType();
                        String queryName = v.getQueryField();
                        String nestedPath = v.getNestedPath();
                        BoolQueryBuilder nestedMustBoolQuery = null;
                        BoolQueryBuilder nestedMustNotBoolQuery = null;
                        boolean nested = false;
                        if (StringUtils.isNotBlank(nestedPath)) {
                            nested = true;
                            if (nestedMustMap.containsKey(nestedPath)) {
                                nestedMustBoolQuery = nestedMustMap.get(nestedPath);
                            } else {
                                nestedMustBoolQuery = QueryBuilders.boolQuery();
                            }
                            if (nestedMustNotMap.containsKey(nestedPath)) {
                                nestedMustNotBoolQuery = nestedMustNotMap.get(nestedPath);
                            } else {
                                nestedMustNotBoolQuery = QueryBuilders.boolQuery();
                            }
                        }
                        switch (queryType) {
                            case RANGE:
                                RangeQueryBuilder rangeQueryBuilder = new RangeQueryBuilder(queryName);
                                if (k.endsWith(v.getFromSuffix())) {
                                    rangeQueryBuilder.from(queryValue);
                                } else {
                                    rangeQueryBuilder.to(queryValue);
                                }
                                boolFilterBuilder.must(rangeQueryBuilder);
                                break;
                            case FUZZY:
                                if (nested) {
                                    if (k.startsWith(NON_FLAG)) {
                                        nestedMustBoolQuery.mustNot(QueryBuilders.wildcardQuery(queryName, queryValue));
                                    } else {
                                        nestedMustBoolQuery.filter(QueryBuilders.wildcardQuery(queryName,
                                                StringUtils.wrapIfMissing(queryValue, WILDCARD)));
                                    }
                                } else {
                                    if (k.startsWith(NON_FLAG)) {
                                        boolFilterBuilder.mustNot(QueryBuilders.wildcardQuery(queryName, queryValue));
                                    } else {
                                        boolFilterBuilder.filter(QueryBuilders.wildcardQuery(queryName,
                                                StringUtils.wrapIfMissing(queryValue, WILDCARD)));
                                    }
                                }
                                break;
                            case PREFIX:
                                boolFilterBuilder.filter(QueryBuilders.prefixQuery(queryName, queryValue));
                                break;
                            case AND:
                                if (nested) {
                                    for (String and : queryValue.split(v.getSeparator())) {
                                        nestedMustBoolQuery.must(QueryBuilders.termQuery(queryName, and));
                                    }
                                } else {
                                    for (String and : queryValue.split(v.getSeparator())) {
                                        boolFilterBuilder.must(QueryBuilders.termQuery(queryName, and));
                                    }
                                }
                                break;
                            case IN:
                                String inQuerySeparator = v.getSeparator();
                                if (nested) {
                                    buildIn(k, queryValue, queryName, nestedMustBoolQuery, inQuerySeparator, nestedMustNotBoolQuery);
                                } else {
                                    buildIn(k, queryValue, queryName, boolFilterBuilder, inQuerySeparator);
                                }
                                break;
                            case SHOULD:
                                boolFilterBuilder.should(QueryBuilders.wildcardQuery(queryName,
                                        StringUtils.wrapIfMissing(queryValue, WILDCARD)));
                                break;
                            case FULLTEXT:
                                if (!Q.equalsIgnoreCase(queryName)) {
                                    fullTextFieldList.add(queryName);
                                }
                                break;
                            case MATCH:
                                boolQueryBuilder.must(QueryBuilders.matchQuery(queryName, queryValue));
                                matchSearch.set(true);
                                break;
                            case EQUAL_IGNORE_CASE:
                                boolFilterBuilder.must(QueryBuilders.termQuery(queryName, queryValue.toLowerCase()));
                                break;
                            case AGGREGATION:
                                searchSourceBuilder.aggregation(AggregationBuilders.terms(v.getKey())
                                        .field(queryName)
                                        .showTermDocCountError(true)
                                );
                                break;
                            default:
                                boolFilterBuilder.must(QueryBuilders.termQuery(queryName, queryValue));
                                break;
                        }
                        if (nested) {
                            if (nestedMustBoolQuery.hasClauses()) {
                                nestedMustMap.put(nestedPath, nestedMustBoolQuery);
                            }
                            if (nestedMustNotBoolQuery.hasClauses()) {
                                nestedMustNotMap.put(nestedPath, nestedMustNotBoolQuery);
                            }
                        }
                    });
            if (StringUtils.isNotBlank(q)) {
                MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(q);
                fullTextFieldList.forEach(multiMatchQueryBuilder::field);
                boolQueryBuilder.should(multiMatchQueryBuilder);
            }
            if (StringUtils.isNotBlank(q) || matchSearch.get()) {
                searchSourceBuilder.sort(SortBuilders.scoreSort().order(SortOrder.DESC));
            }
            if (StringUtils.isNotBlank(missingFields)) {
                for (String miss : missingFields.split(SPLIT_FLAG_COMMA)) {
                    boolFilterBuilder.mustNot(QueryBuilders.existsQuery(miss));
                }
            }
            if (StringUtils.isNotBlank(existsFields)) {
                for (String exists : existsFields.split(SPLIT_FLAG_COMMA)) {
                    boolFilterBuilder.must(QueryBuilders.existsQuery(exists));
                }
            }

            if (!org.springframework.util.CollectionUtils.isEmpty(nestedMustMap)) {
                for (String key : nestedMustMap.keySet()) {
                    if (StringUtils.isBlank(key)) {
                        continue;
                    }
                    boolFilterBuilder.must(QueryBuilders.nestedQuery(key, nestedMustMap.get(key), ScoreMode.None));
                }
            }
            if (!org.springframework.util.CollectionUtils.isEmpty(nestedMustNotMap)) {
                for (String key : nestedMustNotMap.keySet()) {
                    if (StringUtils.isBlank(key)) {
                        continue;
                    }
                    boolFilterBuilder.mustNot(QueryBuilders.nestedQuery(key, nestedMustNotMap.get(key), ScoreMode.None));
                }
            }

            searchSourceBuilder.postFilter(boolFilterBuilder);
            searchSourceBuilder.query(boolQueryBuilder);
            List<SortParam> sortParams = pageParam.getSortParams();
            if (!org.springframework.util.CollectionUtils.isEmpty(sortParams)) {
                for (SortParam sortParam : sortParams) {
                    searchSourceBuilder.sort(SortBuilders.fieldSort(sortParam.getSortField()).order(sortParam.getSortOrder()));
                }
            }
            return searchSourceBuilder;
        } catch (IllegalAccessException e) {
            return null;
        }


    }

    private void buildIn(String k, String queryValue, String queryName, BoolQueryBuilder boolQuery, String separator) {
        buildIn(k, queryValue, queryName, boolQuery, separator, null);
    }

    private void buildIn(String k, String queryValue, String queryName, BoolQueryBuilder boolQuery, String separator,
                         BoolQueryBuilder nestedMustNotBoolQuery) {
        if (queryValue.contains(separator)) {
            if (k.startsWith(NON_FLAG)) {
                if (Objects.nonNull(nestedMustNotBoolQuery)) {
                    nestedMustNotBoolQuery.must(QueryBuilders.termsQuery(queryName, Arrays.asList(queryValue.split(separator))));
                } else {
                    boolQuery.mustNot(QueryBuilders.termsQuery(queryName, Arrays.asList(queryValue.split(separator))));
                }
            } else {
                boolQuery.must(QueryBuilders.termsQuery(queryName, Arrays.asList(queryValue.split(separator))));
            }
        } else {
            if (k.startsWith(NON_FLAG)) {
                if (Objects.nonNull(nestedMustNotBoolQuery)) {
                    nestedMustNotBoolQuery.must(QueryBuilders.termsQuery(queryName, queryValue));
                } else {
                    boolQuery.mustNot(QueryBuilders.termsQuery(queryName, queryValue));
                }
            } else {
                boolQuery.must(QueryBuilders.termsQuery(queryName, queryValue));
            }
        }
    }


}